diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp
index df334bbfe8..225e85e103 100644
--- a/apps/openmw/mwbase/windowmanager.hpp
+++ b/apps/openmw/mwbase/windowmanager.hpp
@@ -11,6 +11,7 @@
 
 #include <MyGUI_KeyCode.h>
 
+#include "../mwgui/mapwindow.hpp"
 #include "../mwgui/mode.hpp"
 
 #include <components/sdlutil/events.hpp>
@@ -237,6 +238,7 @@ namespace MWBase
         virtual void notifyInputActionBound() = 0;
 
         virtual void addVisitedLocation(const std::string& name, int x, int y) = 0;
+        virtual MWGui::CustomMarkerCollection::RangeType getCustomMarkers() const = 0;
 
         /// Hides dialog and schedules dialog to be deleted.
         virtual void removeDialog(std::unique_ptr<MWGui::Layout>&& dialog) = 0;
diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp
index e51350d19f..63021854f5 100644
--- a/apps/openmw/mwgui/windowmanagerimp.cpp
+++ b/apps/openmw/mwgui/windowmanagerimp.cpp
@@ -1652,6 +1652,11 @@ namespace MWGui
         mMap->addVisitedLocation(name, x, y);
     }
 
+    MWGui::CustomMarkerCollection::RangeType WindowManager::getCustomMarkers() const
+    {
+        return std::make_pair(mCustomMarkers.begin(), mCustomMarkers.end());
+    }
+
     const Translation::Storage& WindowManager::getTranslationDataStorage() const
     {
         return mTranslationDataStorage;
diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp
index 052a269188..af33e3bbd2 100644
--- a/apps/openmw/mwgui/windowmanagerimp.hpp
+++ b/apps/openmw/mwgui/windowmanagerimp.hpp
@@ -255,6 +255,7 @@ namespace MWGui
         void notifyInputActionBound() override;
 
         void addVisitedLocation(const std::string& name, int x, int y) override;
+        MWGui::CustomMarkerCollection::RangeType getCustomMarkers() const override;
 
         /// Hides dialog and schedules dialog to be deleted.
         void removeDialog(std::unique_ptr<Layout>&& dialog) override;
diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp
index df317ffeba..66817ae21a 100644
--- a/apps/openmw/mwlua/nearbybindings.cpp
+++ b/apps/openmw/mwlua/nearbybindings.cpp
@@ -12,6 +12,7 @@
 #include "../mwworld/cell.hpp"
 #include "../mwworld/cellstore.hpp"
 #include "../mwworld/scene.hpp"
+#include "../mwworld/worldmodel.hpp"
 
 #include "luamanagerimp.hpp"
 #include "objectlists.hpp"
@@ -41,8 +42,28 @@ namespace
     }
 }
 
+namespace MWLua
+{
+    struct CustomMarker
+    {
+        bool mMutable = false;
+
+        float mWorldX;
+        float mWorldY;
+
+        LCell mCell;
+        std::string mNote;            
+
+    };
+}
+
 namespace sol
 {
+    template <>
+    struct is_automagical<MWLua::CustomMarker> : std::false_type
+    {
+    };
+
     template <>
     struct is_automagical<MWPhysics::RayCastingResult> : std::false_type
     {
@@ -187,6 +208,49 @@ namespace MWLua
         api["doors"] = LObjectList{ objectLists->getDoorsInScene() };
         api["items"] = LObjectList{ objectLists->getItemsInScene() };
         api["players"] = LObjectList{ objectLists->getPlayers() };
+        
+        // FIXME: Make `customMarkers` a dynamic property rather than property of `function` type
+        api["customMarkers"] = sol::readonly_property([context]() {
+            // FIXME: Performance and list caching
+            auto worldModel = MWBase::Environment::get().getWorldModel();
+            auto esmCustomMarkers = MWBase::Environment::get().getWindowManager()->getCustomMarkers();
+            auto luaCustomMarkers = sol::table(context.mLua->sol(), sol::create);
+            size_t i = 0;
+            for (auto& it = esmCustomMarkers.first; it != esmCustomMarkers.second; it++, i++)
+            {
+                auto& esmCustomMarker = it->second;
+
+                LCell cell{ nullptr };
+                if (esmCustomMarker.mCell.is<ESM::ESM3ExteriorCellRefId>())
+                {
+                    cell.mStore = &worldModel->getCell(esmCustomMarker.mCell);
+                }
+                else if (esmCustomMarker.mCell.is<ESM::StringRefId>())
+                {
+                    cell.mStore = worldModel->findInterior(esmCustomMarker.mCell.getRefIdString());
+                }
+                else
+                {
+                    // FIXME: Can it ever happen? What kind of error reporting to use?
+                }
+
+                luaCustomMarkers[i + 1] = CustomMarker{
+                    .mWorldX = esmCustomMarker.mWorldX,
+                    .mWorldY = esmCustomMarker.mWorldY,
+                    .mCell = std::move(cell),
+                    .mNote = esmCustomMarker.mNote,
+                };
+            }
+
+            return LuaUtil::makeReadOnly(luaCustomMarkers);
+        });
+
+        sol::usertype<CustomMarker> customMarkerType = context.mLua->sol().new_usertype<CustomMarker>("CustomMarker");
+        customMarkerType[sol::meta_function::to_string] = [](const CustomMarker& m) { return "CustomMarker[x=" + std::to_string(m.mWorldX) + ", y=" + std::to_string(m.mWorldY) + "]"; };
+        customMarkerType["worldX"] = sol::readonly_property([](const CustomMarker& m) { return m.mWorldX; });
+        customMarkerType["worldY"] = sol::readonly_property([](const CustomMarker& m) { return m.mWorldY; });
+        customMarkerType["cell"] = sol::readonly_property([](const CustomMarker& m) { return m.mCell; });
+        customMarkerType["note"] = sol::readonly_property([](const CustomMarker& m) { return m.mNote; });
 
         api["NAVIGATOR_FLAGS"]
             = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs<std::string_view, DetourNavigator::Flag>(lua,