diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 3b0e5e84185f8bc0c248b8ebca9c6f1713fd1126..a907e37e43b49f318d45cdb5e02183341ea56015 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -177,6 +177,7 @@ add_executable (mpi_unit_tests
   test_ItemValueUtils.cpp
   test_MeshFaceBoundary.cpp
   test_MeshFlatFaceBoundary.cpp
+  test_MeshNodeBoundary.cpp
   test_Messenger.cpp
   test_OFStream.cpp
   test_Partitioner.cpp
diff --git a/tests/test_MeshNodeBoundary.cpp b/tests/test_MeshNodeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ba36ad53f6860523e42f65c33143eb66bd8047bf
--- /dev/null
+++ b/tests/test_MeshNodeBoundary.cpp
@@ -0,0 +1,353 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshNodeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshNodeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a.size() == b.size()) and (&(a[0]) == &(b[0]));
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_node_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_node_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("1D")
+  {
+    static constexpr size_t Dimension = 1;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, named_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+
+    SECTION("unordered 1d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN", "XMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor named_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, named_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+  }
+
+  SECTION("2D")
+  {
+    static constexpr size_t Dimension = 2;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0, 1, 2, 3, 10, 11, 12, 13};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN",     "XMAX",     "YMIN",     "YMAX",
+                                                "XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 2d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {1, 2, 3, 4, 8, 9, 10, 11};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {"XMIN",     "YMIN",     "XMAX",     "YMIN",
+                                                "XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+  }
+
+  SECTION("3D")
+  {
+    static constexpr size_t Dimension = 3;
+
+    using ConnectivityType = Connectivity<Dimension>;
+    using MeshType         = Mesh<ConnectivityType>;
+
+    SECTION("cartesian 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {0,  1,  2,  3,  4,  5,  10, 11, 12, 13, 14, 15, 16,
+                                          17, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {// faces
+                                                "XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX",
+                                                // ridges
+                                                "XMINYMIN", "XMINYMAX", "XMINZMIN", "XMINZMAX", "XMAXYMIN", "XMAXYMAX",
+                                                "XMAXZMIN", "XMAXZMAX", "YMINZMIN", "YMINZMAX", "YMAXZMIN", "YMAXZMAX",
+                                                // corners
+                                                "XMINYMINZMIN", "XMINYMINZMAX", "XMINYMAXZMIN", "XMINYMAXZMAX",
+                                                "XMAXYMINZMIN", "XMAXYMINZMAX", "XMAXYMAXZMIN", "XMAXYMAXZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+
+    SECTION("hybrid 3d")
+    {
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      const ConnectivityType& connectivity = mesh.connectivity();
+
+      {
+        const std::set<size_t> tag_set = {22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
+                                          35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 51};
+
+        for (auto tag : tag_set) {
+          NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_tag(tag, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+
+      {
+        const std::set<std::string> name_set = {// faces
+                                                "XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX",
+                                                // ridges
+                                                "XMINYMIN", "XMINYMAX", "XMINZMIN", "XMINZMAX", "XMAXYMIN", "XMAXYMAX",
+                                                "XMAXZMIN", "XMAXZMAX", "YMINZMIN", "YMINZMAX", "YMAXZMIN", "YMAXZMAX",
+                                                // corners
+                                                "XMINYMINZMIN", "XMINYMINZMAX", "XMINYMAXZMIN", "XMINYMAXZMAX",
+                                                "XMAXYMINZMIN", "XMAXYMINZMAX", "XMAXYMAXZMIN", "XMAXYMAXZMAX"};
+
+        for (auto name : name_set) {
+          NamedBoundaryDescriptor numbered_boundary_descriptor(name);
+          const auto& node_boundary = getMeshNodeBoundary(mesh, numbered_boundary_descriptor);
+
+          auto node_list = get_node_list_from_name(name, connectivity);
+          REQUIRE(is_same(node_boundary.nodeList(), node_list));
+        }
+      }
+    }
+  }
+
+  SECTION("errors")
+  {
+    SECTION("cannot find boundary")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+      const MeshType& mesh   = *p_mesh;
+
+      NamedBoundaryDescriptor named_boundary_descriptor("invalid_boundary");
+
+      REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                          "error: cannot find node list with name \"invalid_boundary\"");
+    }
+
+    SECTION("surface is inside")
+    {
+      SECTION("1D")
+      {
+        static constexpr size_t Dimension = 1;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().unordered1DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE\": inner nodes cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("2D")
+      {
+        static constexpr size_t Dimension = 2;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE");
+
+        REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE\": inner edges cannot be used to define mesh "
+                            "boundaries");
+      }
+
+      SECTION("3D")
+      {
+        static constexpr size_t Dimension = 3;
+
+        using ConnectivityType = Connectivity<Dimension>;
+        using MeshType         = Mesh<ConnectivityType>;
+
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        NamedBoundaryDescriptor named_boundary_descriptor("INTERFACE1");
+
+        REQUIRE_THROWS_WITH(getMeshNodeBoundary(mesh, named_boundary_descriptor),
+                            "error: invalid boundary \"INTERFACE1\": inner faces cannot be used to define mesh "
+                            "boundaries");
+      }
+    }
+  }
+}