diff --git a/DOCS.md b/DOCS.md
index b2ff994f0..cefad5490 100644
--- a/DOCS.md
+++ b/DOCS.md
@@ -572,6 +572,14 @@ Open selected agenda item in the same buffer
 #### **org_agenda_goto**
 *mapped to*: `{'<TAB>'}`<br />
 Open selected agenda item in split window
+#### **org_agenda_open_link**
+*mapped to*: `<Leader>oo`<br />
+Open hyperlink under cursor.<br />
+Hyperlink types supported:
+* URL (http://, https://)
+* File (starts with `file:`. Example: `file:/home/user/.config/nvim/init.lua`) Optionally, a line number can be specified
+using the '+' character. Example: `file:/home/user/.config/nvim/init.lua +10`
+* Fallback: If file path, opens the file.<br />
 #### **org_agenda_goto_date**
 *mapped to*: `J`<br />
 Open calendar that allows selecting date to jump to
diff --git a/lua/orgmode/agenda/init.lua b/lua/orgmode/agenda/init.lua
index a3a5677d2..69ec26024 100644
--- a/lua/orgmode/agenda/init.lua
+++ b/lua/orgmode/agenda/init.lua
@@ -9,6 +9,7 @@ local AgendaSearchView = require('orgmode.agenda.views.search')
 local AgendaTodosView = require('orgmode.agenda.views.todos')
 local AgendaTagsView = require('orgmode.agenda.views.tags')
 local AgendaView = require('orgmode.agenda.views.agenda')
+local Hyperlinks = require('orgmode.org.hyperlinks')
 
 ---@class Agenda
 ---@field content table[]
@@ -245,6 +246,71 @@ function Agenda:change_todo_state()
   })
 end
 
+function Agenda:open_link()
+  local link = Hyperlinks.get_link_under_cursor()
+  if not link then
+    return
+  end
+  local parts = vim.split(link, '][', true)
+  local url = parts[1]
+  local link_ctx = { base = url, skip_add_prefix = true }
+  -- file links
+  if url:find('^file:') then
+    if url:find(' +', 1, true) then
+      parts = vim.split(url, ' +', true)
+      url = parts[1]
+      local line_number = parts[2]
+      vim.cmd(string.format('edit +%s %s', line_number, url:sub(6)))
+      vim.cmd([[normal! zv]])
+      return
+    end
+
+    if url:find('^file:(.-)::') then
+      link_ctx.line = url
+    else
+      vim.cmd(string.format('edit %s', url:sub(6)))
+      vim.cmd([[normal! zv]])
+      return
+    end
+  end
+  -- web links
+  if url:find('^https?://') then
+    if not vim.g.loaded_netrwPlugin then
+      return utils.echo_warning('Netrw plugin must be loaded in order to open urls.')
+    end
+    return vim.fn['netrw#BrowseX'](url, vim.fn['netrw#CheckIfRemote']())
+  end
+  -- fallback: filepath
+  local stat = vim.loop.fs_stat(url)
+  if stat and stat.type == 'file' then
+    return vim.cmd(string.format('edit %s', url))
+  end
+  -- headline link
+  local headlines = Hyperlinks.find_matching_links(link_ctx)
+  if #headlines == 0 then
+    utils.echo_warning('foobar')
+    return
+  end
+  local headline = headlines[1]
+  if #headlines > 1 then
+    local longest_headline = utils.reduce(headlines, function(acc, h)
+      return math.max(acc, h.line:len())
+    end, 0)
+    local options = {}
+    for i, h in ipairs(headlines) do
+      table.insert(options, string.format('%d) %-' .. longest_headline .. 's (%s)', i, h.line, h.file))
+    end
+    vim.cmd([[echo "Multiple targets found. Select target:"]])
+    local choice = vim.fn.inputlist(options)
+    if choice < 1 or choice > #headlines then
+      return
+    end
+    headline = headlines[choice]
+  end
+  vim.cmd(string.format('edit %s', headline.file))
+  vim.fn.cursor(headline.range.start_line, 0)
+end
+
 function Agenda:clock_in()
   return self:_remote_edit({
     action = 'clock.org_clock_in',
diff --git a/lua/orgmode/config/defaults.lua b/lua/orgmode/config/defaults.lua
index 87a1e08c9..543dfc7cd 100644
--- a/lua/orgmode/config/defaults.lua
+++ b/lua/orgmode/config/defaults.lua
@@ -67,6 +67,7 @@ return {
       org_agenda_quit = 'q',
       org_agenda_switch_to = '<CR>',
       org_agenda_goto = '<TAB>',
+      org_agenda_open_link = '<prefix>o',
       org_agenda_goto_date = 'J',
       org_agenda_redo = 'r',
       org_agenda_todo = 't',
diff --git a/lua/orgmode/config/mappings/init.lua b/lua/orgmode/config/mappings/init.lua
index 97dd5762a..852c85671 100644
--- a/lua/orgmode/config/mappings/init.lua
+++ b/lua/orgmode/config/mappings/init.lua
@@ -19,6 +19,7 @@ return {
       { opts = { desc = 'org open agenda item (same buffer)' } }
     ),
     org_agenda_goto = m.action('agenda.goto_item', { opts = { desc = 'org open agenda item (split buffer)' } }),
+    org_agenda_open_link = m.action('agenda.open_link', { opts = { desc = 'org open hyperlink' } }),
     org_agenda_goto_date = m.action('agenda.goto_date', { opts = { desc = 'org goto date' } }),
     org_agenda_redo = m.action('agenda.redo', { opts = { desc = 'org redo' } }),
     org_agenda_todo = m.action('agenda.change_todo_state', { opts = { desc = 'org cycle todo state' } }),
diff --git a/lua/orgmode/org/hyperlinks.lua b/lua/orgmode/org/hyperlinks.lua
index d4482d271..e2c7e34be 100644
--- a/lua/orgmode/org/hyperlinks.lua
+++ b/lua/orgmode/org/hyperlinks.lua
@@ -2,6 +2,24 @@ local Files = require('orgmode.parser.files')
 local utils = require('orgmode.utils')
 local Hyperlinks = {}
 
+---@return string|nil
+function Hyperlinks.get_link_under_cursor()
+  local found_link = nil
+  local links = {}
+  local line = vim.fn.getline('.')
+  local col = vim.fn.col('.')
+  for link in line:gmatch('%[%[(.-)%]%]') do
+    local start_from = #links > 0 and links[#links].to or nil
+    local from, to = line:find('%[%[(.-)%]%]', start_from)
+    if col >= from and col <= to then
+      found_link = link
+      break
+    end
+    table.insert(links, { link = link, from = from, to = to })
+  end
+  return found_link
+end
+
 local function get_file_from_context(ctx)
   return (
     ctx.hyperlinks and ctx.hyperlinks.filepath and Files.get(ctx.hyperlinks.filepath, true)
diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua
index 5408752ae..7a4f64537 100644
--- a/lua/orgmode/org/mappings.lua
+++ b/lua/orgmode/org/mappings.lua
@@ -650,7 +650,7 @@ function OrgMappings:open_at_point()
     return self.agenda:open_day(date)
   end
 
-  local link = self:_get_link_under_cursor()
+  local link = Hyperlinks.get_link_under_cursor()
   if not link then
     return
   end
@@ -910,22 +910,4 @@ function OrgMappings:_adjust_date(amount, span, fallback)
   return vim.api.nvim_feedkeys(utils.esc(fallback), 'n', true)
 end
 
----@return string|nil
-function OrgMappings:_get_link_under_cursor()
-  local found_link = nil
-  local links = {}
-  local line = vim.fn.getline('.')
-  local col = vim.fn.col('.')
-  for link in line:gmatch('%[%[(.-)%]%]') do
-    local start_from = #links > 0 and links[#links].to or nil
-    local from, to = line:find('%[%[(.-)%]%]', start_from)
-    if col >= from and col <= to then
-      found_link = link
-      break
-    end
-    table.insert(links, { link = link, from = from, to = to })
-  end
-  return found_link
-end
-
 return OrgMappings