diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp
index 431efd9032b..34b960b7390 100644
--- a/cpp/open3d/t/geometry/TriangleMesh.cpp
+++ b/cpp/open3d/t/geometry/TriangleMesh.cpp
@@ -1347,6 +1347,163 @@ TriangleMesh TriangleMesh::RemoveUnreferencedVertices() {
     return *this;
 }
 
+template <typename T,
+          typename std::enable_if<std::is_integral<T>::value &&
+                                          !std::is_same<T, bool>::value,
+                                  T>::type * = nullptr>
+using Edge = std::tuple<T, T>;
+
+/// brief Helper function to get an edge with ordered vertex indices.
+template <typename T>
+static inline Edge<T> GetOrderedEdge(T vidx0, T vidx1) {
+    return (vidx0 < vidx1) ? Edge<T>{vidx0, vidx1} : Edge<T>{vidx1, vidx0};
+}
+
+/// brief Helper
+///
+template <typename T>
+static std::unordered_map<Edge<T>,
+                          std::vector<size_t>,
+                          utility::hash_tuple<Edge<T>>>
+GetEdgeToTrianglesMap(const core::Tensor &tris_cpu) {
+    std::unordered_map<Edge<T>, std::vector<size_t>,
+                       utility::hash_tuple<Edge<T>>>
+            tris_per_edge;
+    auto AddEdge = [&](T vidx0, T vidx1, int64_t tidx) {
+        tris_per_edge[GetOrderedEdge(vidx0, vidx1)].push_back(tidx);
+    };
+    const T *tris_ptr = tris_cpu.GetDataPtr<T>();
+    for (int64_t tidx = 0; tidx < tris_cpu.GetLength(); ++tidx) {
+        const T *triangle = &tris_ptr[3 * tidx];
+        AddEdge(triangle[0], triangle[1], tidx);
+        AddEdge(triangle[1], triangle[2], tidx);
+        AddEdge(triangle[2], triangle[0], tidx);
+    }
+    return tris_per_edge;
+}
+
+TriangleMesh TriangleMesh::RemoveNonManifoldEdges() {
+    if (!HasVertexPositions() || GetVertexPositions().GetLength() == 0) {
+        utility::LogWarning(
+                "[RemoveNonManifildEdges] TriangleMesh has no vertices.");
+        return *this;
+    }
+
+    if (!HasTriangleIndices() || GetTriangleIndices().GetLength() == 0) {
+        utility::LogWarning(
+                "[RemoveNonManifoldEdges] TriangleMesh has no triangles.");
+        return *this;
+    }
+
+    GetVertexAttr().AssertSizeSynchronized();
+    GetTriangleAttr().AssertSizeSynchronized();
+
+    core::Tensor tris_cpu =
+            GetTriangleIndices().To(core::Device()).Contiguous();
+
+    ComputeTriangleAreas();
+    core::Tensor tri_areas_cpu =
+            GetTriangleAttr("areas").To(core::Device()).Contiguous();
+
+    DISPATCH_FLOAT_INT_DTYPE_TO_TEMPLATE(
+            GetVertexPositions().GetDtype(), tris_cpu.GetDtype(), [&]() {
+                scalar_t *tri_areas_ptr = tri_areas_cpu.GetDataPtr<scalar_t>();
+                auto edges_to_tris = GetEdgeToTrianglesMap<int_t>(tris_cpu);
+
+                // lambda to compare triangles areas by index
+                auto area_greater_compare = [&tri_areas_ptr](size_t lhs,
+                                                             size_t rhs) {
+                    return tri_areas_ptr[lhs] > tri_areas_ptr[rhs];
+                };
+
+                // go through all edges and for those that have more than 2
+                // triangles attached, remove the triangles with the minimal
+                // area
+                for (auto &kv : edges_to_tris) {
+                    // remove all triangles which are already marked for removal
+                    // (area < 0) note, the erasing of triangles happens
+                    // afterwards
+                    auto tris_end = std::remove_if(
+                            kv.second.begin(), kv.second.end(),
+                            [=](size_t t) { return tri_areas_ptr[t] < 0; });
+                    // count non-removed triangles (with area > 0).
+                    int n_tris = std::distance(kv.second.begin(), tris_end);
+
+                    if (n_tris <= 2) {
+                        // nothing to do here as either:
+                        // - all triangles of the edge are already marked for
+                        // deletion
+                        // - the edge is manifold: it has 1 or 2 triangles with
+                        //   a non-negative area
+                        continue;
+                    }
+
+                    // now erase all triangle indices already marked for removal
+                    kv.second.erase(tris_end, kv.second.end());
+
+                    // find first to triangles with the maximal area
+                    std::nth_element(kv.second.begin(), kv.second.begin() + 1,
+                                     kv.second.end(), area_greater_compare);
+
+                    // mark others for deletion
+                    for (auto it = kv.second.begin() + 2; it < kv.second.end();
+                         ++it) {
+                        tri_areas_ptr[*it] = -1;
+                    }
+                }
+            });
+
+    // mask for triangles with positive area
+    core::Tensor tri_mask = tri_areas_cpu.Gt(0.0).To(GetDevice());
+
+    // pick up positive-area triangles (and their attributes)
+    for (auto item : GetTriangleAttr()) {
+        SetTriangleAttr(item.first, item.second.IndexGet({tri_mask}));
+    }
+
+    return *this;
+}
+
+core::Tensor TriangleMesh::GetNonManifoldEdges(
+        bool allow_boundary_edges /* = true */) const {
+    if (!HasVertexPositions()) {
+        utility::LogWarning(
+                "[GetNonManifoldEdges] TriangleMesh has no vertices.");
+        return {};
+    }
+
+    if (!HasTriangleIndices()) {
+        utility::LogWarning(
+                "[GetNonManifoldEdges] TriangleMesh has no triangles.");
+        return {};
+    }
+
+    core::Tensor result;
+    core::Tensor tris_cpu =
+            GetTriangleIndices().To(core::Device()).Contiguous();
+    core::Dtype tri_dtype = tris_cpu.GetDtype();
+
+    DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(tri_dtype, tris, [&]() {
+        auto edges = GetEdgeToTrianglesMap<scalar_tris_t>(tris_cpu);
+        std::vector<scalar_tris_t> non_manifold_edges;
+
+        for (auto &kv : edges) {
+            if ((allow_boundary_edges &&
+                 (kv.second.size() < 1 || kv.second.size() > 2)) ||
+                (!allow_boundary_edges && kv.second.size() != 2)) {
+                non_manifold_edges.push_back(std::get<0>(kv.first));
+                non_manifold_edges.push_back(std::get<1>(kv.first));
+            }
+        }
+
+        result = core::Tensor(non_manifold_edges,
+                              {(long int)non_manifold_edges.size() / 2, 2},
+                              tri_dtype, GetTriangleIndices().GetDevice());
+    });
+
+    return result;
+}
+
 }  // namespace geometry
 }  // namespace t
 }  // namespace open3d
diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h
index 285c353fb78..c2916c987ca 100644
--- a/cpp/open3d/t/geometry/TriangleMesh.h
+++ b/cpp/open3d/t/geometry/TriangleMesh.h
@@ -979,6 +979,20 @@ class TriangleMesh : public Geometry, public DrawableGeometry {
     /// \return The reference to itself.
     TriangleMesh RemoveUnreferencedVertices();
 
+    /// Removes all non-manifold edges, by successively deleting triangles
+    /// with the smallest surface area adjacent to the
+    /// non-manifold edge until the number of adjacent triangles to the edge is
+    /// `<= 2`. If mesh is empty or has no triangles, prints a warning and
+    /// returns immediately. \return The reference to itself.
+    TriangleMesh RemoveNonManifoldEdges();
+
+    /// Returns the non-manifold edges of the triangle mesh.
+    /// If \param allow_boundary_edges is set to false, then also boundary
+    /// edges are returned.
+    /// \return 2d integer tensor with shape {n,2} encoding ordered edges.
+    /// If mesh is empty or has no triangles, returns an empty tensor.
+    core::Tensor GetNonManifoldEdges(bool allow_boundary_edges = true) const;
+
 protected:
     core::Device device_ = core::Device("CPU:0");
     TensorMap vertex_attr_;
diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp
index 815f22525ea..6be8eb86231 100644
--- a/cpp/pybind/t/geometry/trianglemesh.cpp
+++ b/cpp/pybind/t/geometry/trianglemesh.cpp
@@ -1006,6 +1006,22 @@ or has a negative value, it is ignored.
         surface_area = box.compute_triangle_areas().triangle.areas.sum()
 )");
 
+    triangle_mesh.def("remove_non_manifold_edges",
+                      &TriangleMesh::RemoveNonManifoldEdges,
+                      R"(Function that removes all non-manifold edges, by
+successively deleting  triangles with the smallest surface
+area adjacent to the non-manifold edge until the number of
+adjacent triangles to the edge is `<= 2`.
+
+Returns:
+    The mesh.
+)");
+
+    triangle_mesh.def("get_non_manifold_edges",
+                      &TriangleMesh::GetNonManifoldEdges,
+                      "allow_boundary_edges"_a = true,
+                      R"(Returns the list consisting of non-manifold edges.)");
+
 }  // namespace geometry
 }  // namespace t
 }  // namespace open3d
diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp
index e5657256a20..089f85a852d 100644
--- a/cpp/tests/t/geometry/TriangleMesh.cpp
+++ b/cpp/tests/t/geometry/TriangleMesh.cpp
@@ -1358,5 +1358,69 @@ TEST_P(TriangleMeshPermuteDevices, ComputeTriangleAreas) {
     EXPECT_TRUE(t_mesh.GetTriangleAttr("areas").AllClose(
             core::Tensor(areas, {(int)areas.size()}, core::Float64)));
 }
+
+TEST_P(TriangleMeshPermuteDevices, RemoveNonManifoldEdges) {
+    core::Device device = GetParam();
+    t::geometry::TriangleMesh mesh_empty;
+    EXPECT_TRUE(mesh_empty.RemoveNonManifoldEdges().IsEmpty());
+
+    core::Tensor verts = core::Tensor::Init<float>(
+            {
+                    {0.0, 0.0, 0.0},
+                    {1.0, 0.0, 0.0},
+                    {0.0, 0.0, 1.0},
+                    {1.0, 0.0, 1.0},
+                    {0.0, 1.0, 0.0},
+                    {1.0, 1.0, 0.0},
+                    {0.0, 1.0, 1.0},
+                    {1.0, 1.0, 1.0},
+                    {0.0, -0.2, 0.0},
+            },
+            device);
+
+    mesh_empty.SetVertexPositions(verts);
+    EXPECT_TRUE(mesh_empty.GetVertexPositions().AllClose(verts));
+
+    core::Tensor tris = core::Tensor::Init<int64_t>(
+            {{4, 7, 5}, {8, 0, 1}, {8, 0, 1}, {8, 0, 1}, {4, 6, 7}, {0, 2, 4},
+             {2, 6, 4}, {0, 1, 2}, {1, 3, 2}, {1, 5, 7}, {8, 0, 2}, {8, 0, 2},
+             {8, 0, 1}, {1, 7, 3}, {2, 3, 7}, {2, 7, 6}, {8, 0, 2}, {6, 6, 7},
+             {0, 4, 1}, {8, 0, 4}, {1, 4, 5}},
+            device);
+
+    core::Tensor tri_labels = tris * 10;
+
+    t::geometry::TriangleMesh mesh(verts, tris);
+    mesh.SetTriangleAttr("labels", tri_labels);
+
+    geometry::TriangleMesh legacy_mesh = mesh.ToLegacy();
+    core::Tensor expected_edges =
+            core::eigen_converter::EigenVector2iVectorToTensor(
+                    legacy_mesh.GetNonManifoldEdges(), core::Int64, device);
+    EXPECT_TRUE(mesh.GetNonManifoldEdges().AllClose(expected_edges));
+
+    expected_edges = core::eigen_converter::EigenVector2iVectorToTensor(
+            legacy_mesh.GetNonManifoldEdges(true), core::Int64, device);
+    EXPECT_TRUE(mesh.GetNonManifoldEdges(true).AllClose(expected_edges));
+    expected_edges = core::eigen_converter::EigenVector2iVectorToTensor(
+            legacy_mesh.GetNonManifoldEdges(false), core::Int64, device);
+    EXPECT_TRUE(mesh.GetNonManifoldEdges(false).AllClose(expected_edges));
+
+    mesh.RemoveNonManifoldEdges();
+
+    EXPECT_TRUE(mesh.GetNonManifoldEdges(true).AllClose(
+            core::Tensor({0, 2}, core::Int64)));
+
+    EXPECT_TRUE(mesh.GetNonManifoldEdges(false).AllClose(
+            core::Tensor({0, 2}, core::Int64)));
+
+    t::geometry::TriangleMesh box = t::geometry::TriangleMesh::CreateBox();
+    EXPECT_TRUE(mesh.GetVertexPositions().AllClose(verts));
+    EXPECT_TRUE(mesh.GetTriangleIndices().AllClose(box.GetTriangleIndices()));
+    core::Tensor expected_labels = tri_labels.IndexGet(
+            {core::Tensor::Init<bool>({1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0,
+                                       0, 0, 1, 1, 1, 0, 0, 1, 0, 1})});
+    EXPECT_TRUE(mesh.GetTriangleAttr("labels").AllClose(expected_labels));
+}
 }  // namespace tests
 }  // namespace open3d
diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py
index 5f116732533..e79c188d31b 100644
--- a/python/test/t/geometry/test_trianglemesh.py
+++ b/python/test/t/geometry/test_trianglemesh.py
@@ -734,3 +734,38 @@ def test_compute_triangle_areas(device, int_t, float_t):
     ], float_t, device)
     assert torus.compute_triangle_areas().triangle.areas.allclose(
         expected_areas)
+
+
+@pytest.mark.parametrize("device", list_devices())
+@pytest.mark.parametrize("int_t", (o3c.int32, o3c.int64))
+@pytest.mark.parametrize("float_t", (o3c.float32, o3c.float64))
+def test_remove_non_manifold_edges(device, int_t, float_t):
+    verts = o3c.Tensor([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0],
+                        [1.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0],
+                        [0.0, 1.0, 1.0], [1.0, 1.0, 1.0], [0.0, -0.2, 0.0]],
+                       float_t, device)
+
+    tris = o3c.Tensor(
+        [[4, 7, 5], [8, 0, 1], [8, 0, 1], [8, 0, 1], [4, 6, 7], [0, 2, 4],
+         [2, 6, 4], [0, 1, 2], [1, 3, 2], [1, 5, 7], [8, 0, 2], [8, 0, 2],
+         [8, 0, 1], [1, 7, 3], [2, 3, 7], [2, 7, 6], [8, 0, 2], [6, 6, 7],
+         [0, 4, 1], [8, 0, 4], [1, 4, 5]], int_t, device)
+
+    test_box = o3d.t.geometry.TriangleMesh(verts, tris)
+    test_box_legacy = test_box.to_legacy()
+
+    # allow boundary edges
+    edges = test_box_legacy.get_non_manifold_edges()
+    np.testing.assert_allclose(test_box.get_non_manifold_edges().numpy(),
+                               np.asarray(edges))
+    # disallow boundary edges
+    edges = test_box_legacy.get_non_manifold_edges(False)
+    np.testing.assert_allclose(
+        test_box.get_non_manifold_edges(False).numpy(), np.asarray(edges))
+
+    test_box.remove_non_manifold_edges()
+
+    box = o3d.t.geometry.TriangleMesh.create_box(float_dtype=float_t,
+                                                 int_dtype=int_t)
+    assert test_box.vertex.positions.allclose(verts)
+    assert test_box.triangle.indices.allclose(box.triangle.indices)