diff --git a/modules/core/inc/tactile/core/map/layer/object_layer.hpp b/modules/core/inc/tactile/core/map/layer/object_layer.hpp
index 6974965f38..146e6f8119 100644
--- a/modules/core/inc/tactile/core/map/layer/object_layer.hpp
+++ b/modules/core/inc/tactile/core/map/layer/object_layer.hpp
@@ -3,8 +3,13 @@
 #pragma once
 
 #include "tactile/core/api.hpp"
+#include "tactile/core/container/smart_ptr.hpp"
+#include "tactile/core/container/tree_map.hpp"
+#include "tactile/core/container/vector.hpp"
 #include "tactile/core/map/layer/layer.hpp"
 #include "tactile/core/map/layer/layer_behavior_delegate.hpp"
+#include "tactile/core/map/layer/object.hpp"
+#include "tactile/core/misc/id_types.hpp"
 #include "tactile/core/prelude.hpp"
 
 namespace tactile {
@@ -18,12 +23,79 @@ class TACTILE_CORE_API ObjectLayer final : public ILayer {
 
   void accept(ILayerVisitor& visitor) override;
 
+  /**
+   * \brief Adds an object to the layer.
+   *
+   * \note An existing object in the layer with the specified ID will be overwritten.
+   *
+   * \param id     the ID to associate with the object.
+   * \param object the object to add.
+   */
+  void add_object(ObjectID id, Shared<Object> object);
+
+  /**
+   * \brief Removes an object from the layer.
+   *
+   * \param id the ID associated with the object.
+   *
+   * \return the removed object.
+   */
+  auto remove_object(ObjectID id) -> Shared<Object>;
+
   void set_persistent_id(Maybe<int32> id) override;
 
   void set_opacity(float opacity) override;
 
   void set_visible(bool visible) override;
 
+  /**
+   * \brief Returns an object in the layer.
+   *
+   * \note Avoid calling this function if you only need to "look" at the returned object,
+   *       prefer calling `find_object` in such cases. This avoids expensive copies of
+   *       shared pointers.
+   *
+   * \param id the ID of the desired object.
+   *
+   * \return a shared pointer to the found object.
+   */
+  [[nodiscard]]
+  auto get_object(ObjectID id) -> Shared<Object>;
+
+  /**
+   * \brief Attempts to find and return an object in the layer.
+   *
+   * \param id the ID of the desired object.
+   *
+   * \return a pointer to the found object, or a null pointer.
+   */
+  [[nodiscard]]
+  auto find_object(ObjectID id) -> Object*;
+
+  /**
+   * \copydoc find_object()
+   */
+  [[nodiscard]]
+  auto find_object(ObjectID id) const -> const Object*;
+
+  /**
+   * \brief Indicates whether the layer contains an object with the specified ID.
+   *
+   * \param id the object ID to check.
+   *
+   * \return true if an object was found; false otherwise.
+   */
+  [[nodiscard]]
+  auto has_object(ObjectID id) const -> bool;
+
+  /**
+   * \brief Returns the number of objects in the layer.
+   *
+   * \return an object count.
+   */
+  [[nodiscard]]
+  auto object_count() const -> usize;
+
   [[nodiscard]]
   auto get_persistent_id() const -> Maybe<int32> override;
 
@@ -44,6 +116,7 @@ class TACTILE_CORE_API ObjectLayer final : public ILayer {
 
  private:
   LayerBehaviorDelegate mDelegate;
+  TreeMap<ObjectID, Shared<Object>> mObjects;
 };
 
 }  // namespace tactile
diff --git a/modules/core/src/tactile/core/map/layer/object_layer.cpp b/modules/core/src/tactile/core/map/layer/object_layer.cpp
index ecbe85d4ec..49e605d450 100644
--- a/modules/core/src/tactile/core/map/layer/object_layer.cpp
+++ b/modules/core/src/tactile/core/map/layer/object_layer.cpp
@@ -2,6 +2,9 @@
 
 #include "tactile/core/map/layer/object_layer.hpp"
 
+#include <utility>  // move
+
+#include "tactile/core/container/lookup.hpp"
 #include "tactile/core/map/layer/layer_visitor.hpp"
 
 namespace tactile {
@@ -16,6 +19,20 @@ void ObjectLayer::accept(ILayerVisitor& visitor)
   visitor.visit(*this);
 }
 
+void ObjectLayer::add_object(const ObjectID id, Shared<Object> object)
+{
+  mObjects[id] = std::move(object);
+}
+
+auto ObjectLayer::remove_object(const ObjectID id) -> Shared<Object>
+{
+  if (auto object = erase_from(mObjects, id)) {
+    return std::move(object).value();
+  }
+
+  return nullptr;
+}
+
 void ObjectLayer::set_persistent_id(const Maybe<int32> id)
 {
   mDelegate.set_persistent_id(id);
@@ -31,6 +48,33 @@ void ObjectLayer::set_visible(const bool visible)
   mDelegate.set_visible(visible);
 }
 
+auto ObjectLayer::get_object(const ObjectID id) -> Shared<Object>
+{
+  return lookup_in(mObjects, id);
+}
+
+auto ObjectLayer::find_object(const ObjectID id) -> Object*
+{
+  const auto iter = mObjects.find(id);
+  return (iter != mObjects.end()) ? iter->second.get() : nullptr;
+}
+
+auto ObjectLayer::find_object(const ObjectID id) const -> const Object*
+{
+  const auto iter = mObjects.find(id);
+  return (iter != mObjects.end()) ? iter->second.get() : nullptr;
+}
+
+auto ObjectLayer::has_object(const ObjectID id) const -> bool
+{
+  return mObjects.contains(id);
+}
+
+auto ObjectLayer::object_count() const -> usize
+{
+  return mObjects.size();
+}
+
 auto ObjectLayer::get_persistent_id() const -> Maybe<int32>
 {
   return mDelegate.get_persistent_id();
diff --git a/modules/core/test/map/layer/object_layer_test.cpp b/modules/core/test/map/layer/object_layer_test.cpp
new file mode 100644
index 0000000000..f02f839e8e
--- /dev/null
+++ b/modules/core/test/map/layer/object_layer_test.cpp
@@ -0,0 +1,89 @@
+// Copyright (C) 2023 Albin Johansson (GNU General Public License v3.0)
+
+#include "tactile/core/map/layer/object_layer.hpp"
+
+#include <gtest/gtest.h>
+
+using namespace tactile;
+using tactile::int_literals::operator""_uz;
+
+/// \tests tactile::ObjectLayer::add_object
+TEST(ObjectLayer, AddObject)
+{
+  const ObjectID object_id {42};
+
+  const auto object1 = make_shared<Object>(ObjectType::kRect);
+  const auto object2 = make_shared<Object>(ObjectType::kEllipse);
+
+  ObjectLayer layer;
+  EXPECT_EQ(layer.object_count(), 0_uz);
+
+  layer.add_object(object_id, object1);
+  EXPECT_EQ(layer.object_count(), 1_uz);
+  EXPECT_EQ(layer.get_object(object_id), object1);
+
+  layer.add_object(object_id, object2);
+  EXPECT_EQ(layer.object_count(), 1_uz);
+  EXPECT_EQ(layer.get_object(object_id), object2);
+}
+
+/// \tests tactile::ObjectLayer::remove_object
+/// \tests tactile::ObjectLayer::has_object
+TEST(ObjectLayer, RemoveObject)
+{
+  const ObjectID object1_id {212};
+  const ObjectID object2_id {832};
+
+  const auto object1 = make_shared<Object>(ObjectType::kPoint);
+  const auto object2 = make_shared<Object>(ObjectType::kRect);
+
+  ObjectLayer layer;
+  layer.add_object(object1_id, object1);
+  layer.add_object(object2_id, object2);
+  ASSERT_EQ(layer.object_count(), 2_uz);
+  EXPECT_TRUE(layer.has_object(object1_id));
+  EXPECT_TRUE(layer.has_object(object2_id));
+
+  EXPECT_EQ(layer.remove_object(object1_id), object1);
+  EXPECT_EQ(layer.object_count(), 1_uz);
+  EXPECT_FALSE(layer.has_object(object1_id));
+  EXPECT_TRUE(layer.has_object(object2_id));
+
+  EXPECT_EQ(layer.remove_object(object2_id), object2);
+  EXPECT_EQ(layer.object_count(), 0_uz);
+  EXPECT_FALSE(layer.has_object(object1_id));
+  EXPECT_FALSE(layer.has_object(object2_id));
+
+  EXPECT_EQ(layer.remove_object(object1_id), nullptr);
+  EXPECT_EQ(layer.remove_object(object2_id), nullptr);
+}
+
+/// \tests tactile::ObjectLayer::get_object
+TEST(ObjectLayer, GetObject)
+{
+  const ObjectID object_id {123};
+  const auto object = make_shared<Object>(ObjectType::kPoint);
+
+  ObjectLayer layer;
+  EXPECT_ANY_THROW((void) layer.get_object(object_id));
+
+  layer.add_object(object_id, object);
+  EXPECT_EQ(layer.get_object(object_id), object);
+}
+
+/// \tests tactile::ObjectLayer::find_object
+TEST(ObjectLayer, FindObject)
+{
+  const ObjectID object_id {923};
+  const auto object = make_shared<Object>(ObjectType::kEllipse);
+
+  ObjectLayer layer;
+  const auto& const_layer = layer;
+
+  EXPECT_EQ(layer.find_object(object_id), nullptr);
+  EXPECT_EQ(const_layer.find_object(object_id), nullptr);
+
+  layer.add_object(object_id, object);
+  EXPECT_EQ(layer.find_object(object_id), object.get());
+  EXPECT_EQ(const_layer.find_object(object_id), object.get());
+}