diff --git a/flang/include/flang/Lower/OpenMP.h b/flang/include/flang/Lower/OpenMP.h
--- a/flang/include/flang/Lower/OpenMP.h
+++ b/flang/include/flang/Lower/OpenMP.h
@@ -13,13 +13,9 @@
 #ifndef FORTRAN_LOWER_OPENMP_H
 #define FORTRAN_LOWER_OPENMP_H
 
+#include "mlir/Dialect/OpenMP/OpenMPDialect.h"
 #include <cinttypes>
 
-namespace mlir {
-class Value;
-class Operation;
-} // namespace mlir
-
 namespace fir {
 class FirOpBuilder;
 class ConvertOp;
@@ -29,6 +25,7 @@
 namespace parser {
 struct OpenMPConstruct;
 struct OpenMPDeclarativeConstruct;
+struct OpenMPDeclareTargetConstruct;
 struct OmpEndLoopDirective;
 struct OmpClauseList;
 } // namespace parser
@@ -56,6 +53,17 @@
 void updateReduction(mlir::Operation *, fir::FirOpBuilder &, mlir::Value,
                      mlir::Value, fir::ConvertOp * = nullptr);
 void removeStoreOp(mlir::Operation *, mlir::Value);
+
+std::optional<mlir::omp::DeclareTargetDeviceType>
+getOpenMPDeclareTargetFunctionDevice(
+    Fortran::lower::AbstractConverter &, Fortran::lower::pft::Evaluation &,
+    const Fortran::parser::OpenMPDeclareTargetConstruct &);
+std::optional<mlir::omp::ClauseMemoryOrderKind>
+extractOpenMPRequiresClauses(const Fortran::parser::OmpClauseList &,
+                             mlir::omp::ClauseRequires &);
+
+bool isOpenMPTargetConstruct(const parser::OpenMPConstruct &);
+
 } // namespace lower
 } // namespace Fortran
 
diff --git a/flang/lib/Lower/Bridge.cpp b/flang/lib/Lower/Bridge.cpp
--- a/flang/lib/Lower/Bridge.cpp
+++ b/flang/lib/Lower/Bridge.cpp
@@ -52,6 +52,7 @@
 #include "flang/Semantics/runtime-type-info.h"
 #include "flang/Semantics/tools.h"
 #include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
+#include "mlir/Dialect/OpenMP/OpenMPDialect.h"
 #include "mlir/IR/PatternMatch.h"
 #include "mlir/Parser/Parser.h"
 #include "mlir/Transforms/RegionUtils.h"
@@ -266,7 +267,8 @@
 public:
   explicit FirConverter(Fortran::lower::LoweringBridge &bridge)
       : Fortran::lower::AbstractConverter(bridge.getLoweringOptions()),
-        bridge{bridge}, foldingContext{bridge.createFoldingContext()} {}
+        bridge{bridge}, foldingContext{bridge.createFoldingContext()},
+        ompRequiresFlags{mlir::omp::ClauseRequires::none} {}
   virtual ~FirConverter() = default;
 
   /// Convert the PFT to FIR.
@@ -343,6 +345,16 @@
         fir::runtime::genEnvironmentDefaults(*builder, toLocation(),
                                              bridge.getEnvironmentDefaults());
       });
+
+    // Set the module attributes related to OpenMP requires directives
+    if (auto mod = llvm::dyn_cast<mlir::omp::OffloadModuleInterface>(
+            getModuleOp().getOperation())) {
+      if (ompDeviceCodeFound)
+        mod.setRequires(ompRequiresFlags);
+
+      if (ompAtomicDefaultMemOrder)
+        mod.setAtomicDefaultMemOrder(*ompAtomicDefaultMemOrder);
+    }
   }
 
   /// Declare a function.
@@ -2053,10 +2065,57 @@
 
     localSymbols.popScope();
     builder->restoreInsertionPoint(insertPt);
+
+    // Register if a target region was found
+    ompDeviceCodeFound =
+        ompDeviceCodeFound || Fortran::lower::isOpenMPTargetConstruct(omp);
+  }
+
+  /// Extract information from OpenMP declarative constructs
+  void analyzeOpenMPDeclarative(
+      const Fortran::parser::OpenMPDeclarativeConstruct &ompDecl) {
+    auto analyzeRequires =
+        [&](const Fortran::parser::OpenMPRequiresConstruct &ompReq) {
+          using mlir::omp::ClauseRequires;
+
+          mlir::omp::ClauseRequires requiresFlags;
+          auto atomicDefaultMemOrder =
+              Fortran::lower::extractOpenMPRequiresClauses(
+                  std::get<Fortran::parser::OmpClauseList>(ompReq.t),
+                  requiresFlags);
+
+          if (requiresFlags != ClauseRequires::none)
+            ompRequiresFlags = ompRequiresFlags | requiresFlags;
+
+          if (atomicDefaultMemOrder)
+            ompAtomicDefaultMemOrder = atomicDefaultMemOrder;
+        };
+
+    auto analyzeDeclareTarget =
+        [&](const Fortran::parser::OpenMPDeclareTargetConstruct &ompReq) {
+          auto targetType =
+              Fortran::lower::getOpenMPDeclareTargetFunctionDevice(
+                  *this, getEval(), ompReq);
+
+          ompDeviceCodeFound =
+              ompDeviceCodeFound ||
+              (targetType &&
+               *targetType != mlir::omp::DeclareTargetDeviceType::host);
+        };
+
+    std::visit(
+        Fortran::common::visitors{
+            analyzeRequires,
+            analyzeDeclareTarget,
+            // Add other OpenMP declarative constructs currently skipped
+            [&](const auto &) {},
+        },
+        ompDecl.u);
   }
 
   void genFIR(const Fortran::parser::OpenMPDeclarativeConstruct &ompDecl) {
     mlir::OpBuilder::InsertPoint insertPt = builder->saveInsertionPoint();
+    analyzeOpenMPDeclarative(ompDecl);
     genOpenMPDeclarativeConstruct(*this, getEval(), ompDecl);
     for (Fortran::lower::pft::Evaluation &e : getEval().getNestedEvaluations())
       genFIR(e);
@@ -4098,6 +4157,17 @@
 
   /// Tuple of host associated variables
   mlir::Value hostAssocTuple;
+
+  /// OpenMP Requires flags
+  mlir::omp::ClauseRequires ompRequiresFlags;
+
+  /// OpenMP Default memory order for atomic operations, as defined by a
+  /// 'requires' directive
+  std::optional<mlir::omp::ClauseMemoryOrderKind> ompAtomicDefaultMemOrder;
+
+  /// Whether an OpenMP target region or declare target function/subroutine
+  /// intended for device offloading has been detected
+  bool ompDeviceCodeFound = false;
 };
 
 } // namespace
diff --git a/flang/lib/Lower/OpenMP.cpp b/flang/lib/Lower/OpenMP.cpp
--- a/flang/lib/Lower/OpenMP.cpp
+++ b/flang/lib/Lower/OpenMP.cpp
@@ -24,7 +24,9 @@
 #include "flang/Semantics/tools.h"
 #include "mlir/Dialect/OpenMP/OpenMPDialect.h"
 #include "mlir/Dialect/SCF/IR/SCF.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/Frontend/OpenMP/OMPConstants.h"
+#include <optional>
 
 using namespace mlir;
 
@@ -2239,11 +2241,13 @@
   converter.bindSymbol(sym, symThreadprivateExv);
 }
 
-void handleDeclareTarget(Fortran::lower::AbstractConverter &converter,
-                         Fortran::lower::pft::Evaluation &eval,
-                         const Fortran::parser::OpenMPDeclareTargetConstruct
-                             &declareTargetConstruct) {
-  llvm::SmallVector<Fortran::semantics::Symbol, 0> symbols;
+/// Extract the list of function and variable symbols affected by the given
+/// 'declare target' directive and return the intended device type for them.
+static mlir::omp::DeclareTargetDeviceType getDeclareTargetInfo(
+    Fortran::lower::pft::Evaluation &eval,
+    const Fortran::parser::OpenMPDeclareTargetConstruct &declareTargetConstruct,
+    SmallVectorImpl<Fortran::semantics::Symbol> &symbols) {
+  // Gather the symbols
   auto findFuncAndVarSyms = [&](const Fortran::parser::OmpObjectList &objList) {
     for (const auto &ompObject : objList.v) {
       Fortran::common::visit(
@@ -2261,12 +2265,11 @@
     }
   };
 
+  // The default capture type
+  auto deviceType = Fortran::parser::OmpDeviceTypeClause::Type::Any;
   const auto &spec{std::get<Fortran::parser::OmpDeclareTargetSpecifier>(
       declareTargetConstruct.t)};
-  auto mod = converter.getFirOpBuilder().getModule();
 
-  // The default capture type
-  auto deviceType = Fortran::parser::OmpDeviceTypeClause::Type::Any;
   if (const auto *objectList{
           Fortran::parser::Unwrap<Fortran::parser::OmpObjectList>(spec.u)}) {
     // Case: declare target(func, var1, var2)
@@ -2288,8 +2291,7 @@
                      std::get_if<Fortran::parser::OmpClause::Link>(
                          &clause.u)}) {
         // Case: declare target link(var1, var2)...
-        TODO(converter.getCurrentLocation(),
-             "the link clause is currently unsupported");
+        TODO_NOLOC("the link clause is currently unsupported");
       } else if (const auto *deviceClause{
                      std::get_if<Fortran::parser::OmpClause::DeviceType>(
                          &clause.u)}) {
@@ -2299,6 +2301,24 @@
     }
   }
 
+  switch (deviceType) {
+  case Fortran::parser::OmpDeviceTypeClause::Type::Any:
+    return mlir::omp::DeclareTargetDeviceType::any;
+  case Fortran::parser::OmpDeviceTypeClause::Type::Host:
+    return mlir::omp::DeclareTargetDeviceType::host;
+  case Fortran::parser::OmpDeviceTypeClause::Type::Nohost:
+    return mlir::omp::DeclareTargetDeviceType::nohost;
+  }
+}
+
+void handleDeclareTarget(Fortran::lower::AbstractConverter &converter,
+                         Fortran::lower::pft::Evaluation &eval,
+                         const Fortran::parser::OpenMPDeclareTargetConstruct
+                             &declareTargetConstruct) {
+  SmallVector<Fortran::semantics::Symbol, 0> symbols;
+  auto deviceType = getDeclareTargetInfo(eval, declareTargetConstruct, symbols);
+
+  auto mod = converter.getFirOpBuilder().getModule();
   for (auto sym : symbols) {
     auto *op = mod.lookupSymbol(converter.mangleName(sym));
 
@@ -2309,20 +2329,6 @@
       TODO(converter.getCurrentLocation(),
            "only subroutines and functions are currently supported");
 
-    mlir::omp::DeclareTargetDeviceType newDeviceType;
-
-    switch (deviceType) {
-    case Fortran::parser::OmpDeviceTypeClause::Type::Nohost:
-      newDeviceType = mlir::omp::DeclareTargetDeviceType::nohost;
-      break;
-    case Fortran::parser::OmpDeviceTypeClause::Type::Host:
-      newDeviceType = mlir::omp::DeclareTargetDeviceType::host;
-      break;
-    case Fortran::parser::OmpDeviceTypeClause::Type::Any:
-      newDeviceType = mlir::omp::DeclareTargetDeviceType::any;
-      break;
-    }
-
     // The function already has a declare target applied to it, very
     // likely through implicit capture (usage in another declare target
     // function/subroutine). It should be marked as any if it has been
@@ -2330,13 +2336,13 @@
     // change
     if (mlir::omp::OpenMPDialect::isDeclareTarget(fOp)) {
       if (mlir::omp::OpenMPDialect::getDeclareTargetDeviceType(fOp) !=
-          newDeviceType)
+          deviceType)
         mlir::omp::OpenMPDialect::setDeclareTarget(
             op, mlir::omp::DeclareTargetDeviceType::any);
       continue;
     }
 
-    mlir::omp::OpenMPDialect::setDeclareTarget(op, newDeviceType);
+    mlir::omp::OpenMPDialect::setDeclareTarget(op, deviceType);
   }
 }
 
@@ -2366,7 +2372,11 @@
           },
           [&](const Fortran::parser::OpenMPRequiresConstruct
                   &requiresConstruct) {
-            TODO(converter.getCurrentLocation(), "OpenMPRequiresConstruct");
+            // Requires directives are analyzed before any statements are
+            // lowered. Then, the result of combining the set of clauses of all
+            // requires directives present in the compilation unit is used to
+            // emit code, so no code is emitted independently for each
+            // "requires" instance.
           },
           [&](const Fortran::parser::OpenMPThreadprivate &threadprivate) {
             // The directive is lowered when instantiating the variable to
@@ -2576,3 +2586,93 @@
     }
   }
 }
+
+std::optional<mlir::omp::DeclareTargetDeviceType>
+Fortran::lower::getOpenMPDeclareTargetFunctionDevice(
+    Fortran::lower::AbstractConverter &converter,
+    Fortran::lower::pft::Evaluation &eval,
+    const Fortran::parser::OpenMPDeclareTargetConstruct
+        &declareTargetConstruct) {
+  SmallVector<Fortran::semantics::Symbol, 0> symbols;
+  auto deviceType = getDeclareTargetInfo(eval, declareTargetConstruct, symbols);
+
+  // Return the device type only if at least one of the targets for the
+  // directive is a function or subroutine
+  auto mod = converter.getFirOpBuilder().getModule();
+  for (auto sym : symbols) {
+    auto *op = mod.lookupSymbol(converter.mangleName(sym));
+
+    if (mlir::isa<mlir::func::FuncOp>(op))
+      return deviceType;
+  }
+
+  return std::nullopt;
+}
+
+std::optional<omp::ClauseMemoryOrderKind>
+Fortran::lower::extractOpenMPRequiresClauses(
+    const Fortran::parser::OmpClauseList &clauseList,
+    omp::ClauseRequires &requiresFlags) {
+  std::optional<omp::ClauseMemoryOrderKind> atomicDefaultMemOrder;
+  requiresFlags = omp::ClauseRequires::none;
+
+  for (const auto &clause : clauseList.v) {
+    if (const auto &atomicClause =
+            std::get_if<Fortran::parser::OmpClause::AtomicDefaultMemOrder>(
+                &clause.u)) {
+      switch (atomicClause->v.v) {
+      case Fortran::parser::OmpAtomicDefaultMemOrderClause::Type::SeqCst:
+        atomicDefaultMemOrder = omp::ClauseMemoryOrderKind::Seq_cst;
+        break;
+      case Fortran::parser::OmpAtomicDefaultMemOrderClause::Type::AcqRel:
+        atomicDefaultMemOrder = omp::ClauseMemoryOrderKind::Acq_rel;
+        break;
+      case Fortran::parser::OmpAtomicDefaultMemOrderClause::Type::Relaxed:
+        atomicDefaultMemOrder = omp::ClauseMemoryOrderKind::Relaxed;
+        break;
+      }
+    } else if (std::get_if<Fortran::parser::OmpClause::DynamicAllocators>(
+                   &clause.u)) {
+      requiresFlags = requiresFlags | omp::ClauseRequires::dynamic_allocators;
+    } else if (std::get_if<Fortran::parser::OmpClause::ReverseOffload>(
+                   &clause.u)) {
+      requiresFlags = requiresFlags | omp::ClauseRequires::reverse_offload;
+    } else if (std::get_if<Fortran::parser::OmpClause::UnifiedAddress>(
+                   &clause.u)) {
+      requiresFlags = requiresFlags | omp::ClauseRequires::unified_address;
+    } else if (std::get_if<Fortran::parser::OmpClause::UnifiedSharedMemory>(
+                   &clause.u)) {
+      requiresFlags =
+          requiresFlags | omp::ClauseRequires::unified_shared_memory;
+    }
+  }
+
+  return atomicDefaultMemOrder;
+}
+
+bool Fortran::lower::isOpenMPTargetConstruct(
+    const Fortran::parser::OpenMPConstruct &omp) {
+  if (const auto *blockDir =
+          std::get_if<Fortran::parser::OpenMPBlockConstruct>(&omp.u)) {
+    const auto &beginBlockDir{
+        std::get<Fortran::parser::OmpBeginBlockDirective>(blockDir->t)};
+    const auto &beginDir{
+        std::get<Fortran::parser::OmpBlockDirective>(beginBlockDir.t)};
+
+    switch (beginDir.v) {
+    case llvm::omp::Directive::OMPD_target:
+    case llvm::omp::Directive::OMPD_target_parallel:
+    case llvm::omp::Directive::OMPD_target_parallel_do:
+    case llvm::omp::Directive::OMPD_target_parallel_do_simd:
+    case llvm::omp::Directive::OMPD_target_simd:
+    case llvm::omp::Directive::OMPD_target_teams:
+    case llvm::omp::Directive::OMPD_target_teams_distribute:
+    case llvm::omp::Directive::OMPD_target_teams_distribute_simd:
+      return true;
+    default:
+      break;
+    }
+  }
+
+  return false;
+}
diff --git a/flang/test/Lower/OpenMP/requires-error.f90 b/flang/test/Lower/OpenMP/requires-error.f90
new file mode 100644
--- /dev/null
+++ b/flang/test/Lower/OpenMP/requires-error.f90
@@ -0,0 +1,15 @@
+! RUN: not %flang_fc1 -emit-fir -fopenmp %s -o - 2>&1 | FileCheck %s
+
+! This test checks that requires lowering into MLIR skips creating the
+! omp.requires attribute with target-related clauses if there are no device
+! functions in the compilation unit
+
+!CHECK:      error: {{.*}} conflicting atomic_default_mem_order clause found:
+!CHECK-SAME: acq_rel != seq_cst
+program requires
+  !$omp requires atomic_default_mem_order(seq_cst)
+end program requires
+
+subroutine f()
+  !$omp requires atomic_default_mem_order(acq_rel)
+end subroutine f
diff --git a/flang/test/Lower/OpenMP/requires-notarget.f90 b/flang/test/Lower/OpenMP/requires-notarget.f90
new file mode 100644
--- /dev/null
+++ b/flang/test/Lower/OpenMP/requires-notarget.f90
@@ -0,0 +1,12 @@
+! RUN: %flang_fc1 -emit-fir -fopenmp %s -o - | FileCheck %s
+
+! This test checks that requires lowering into MLIR skips creating the
+! omp.requires attribute with target-related clauses if there are no device
+! functions in the compilation unit
+
+!CHECK:      module attributes {
+!CHECK-SAME: omp.atomic_default_mem_order = #omp<memoryorderkind seq_cst>
+!CHECK-NOT:  omp.requires
+program requires
+  !$omp requires unified_shared_memory reverse_offload atomic_default_mem_order(seq_cst)
+end program requires
diff --git a/flang/test/Lower/OpenMP/requires.f90 b/flang/test/Lower/OpenMP/requires.f90
new file mode 100644
--- /dev/null
+++ b/flang/test/Lower/OpenMP/requires.f90
@@ -0,0 +1,14 @@
+! RUN: %flang_fc1 -emit-fir -fopenmp %s -o - | FileCheck %s
+
+! This test checks the lowering of requires into MLIR
+
+!CHECK:      module attributes {
+!CHECK-SAME: omp.atomic_default_mem_order = #omp<memoryorderkind seq_cst>
+!CHECK-SAME: omp.requires = #omp<clause_requires reverse_offload|unified_shared_memory>
+program requires
+  !$omp requires unified_shared_memory reverse_offload atomic_default_mem_order(seq_cst)
+end program requires
+
+subroutine f
+  !$omp declare target
+end subroutine f